package procilege.impl.expand;

import static java.lang.String.format;
import static java.util.logging.Level.FINEST;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Stack;
import java.util.logging.Logger;

import org.apache.solr.util.OpenBitSet;

import procilege.compute.ExprExpandTraveller;
import procilege.compute.ExprFormatter;
import procilege.compute.PlaceType;
import procilege.compute.TravelMap;
import procilege.lang.BoolExprElement;
import procilege.lang.EntityExprElement;
import procilege.lang.ExprElement;
import procilege.lang.Name;
import procilege.lang.PrivExprComposition;
import procilege.lang.PrivExprElement;
import procilege.lang.PrivExprMergence;

import com.google.inject.Inject;
import com.google.inject.Injector;

public class ExprExpandTravellerImpl implements ExprExpandTraveller {

	@Inject
	private Injector injector;

	@Inject
	private ExprFormatter ftr;

	@Inject
	private Logger logger;

	private TravelMap<ExprExpandTraveller> travelmap;

	private Stack<SetoutStackElement> setoutStack = new Stack<SetoutStackElement>();

	private HashMap<Long, Collection<PrivExprMergence>> applications = new HashMap<Long, Collection<PrivExprMergence>>();

	private Name bearer;

	private PrivExprMergence nearestMergence = null;

	private int footprint = 0; // for debugging

	private HashSet<Class<?>> included;

	ExprExpandTravellerImpl(ExprExpandTravellerImpl tr, OpenBitSet boundnames) {

		this.boundnames = new OpenBitSet();
		this.boundnames.or(tr.boundnames);
		this.boundnames.or(boundnames);

		this.travelmap = tr.travelmap;

		setoutStack.push(new SetoutStackElement());
	}

	ExprExpandTravellerImpl(ExprExpandTravellerImpl tr) {

		this.boundnames = new OpenBitSet();
		this.boundnames.or(tr.boundnames);

		this.travelmap = tr.travelmap;

		setoutStack.push(new SetoutStackElement());
	}

	ExprExpandTravellerImpl(TravelMap<ExprExpandTraveller> travelmap,
			OpenBitSet boundnames) {

		this.boundnames = boundnames;

		this.travelmap = travelmap;

		setoutStack.push(new SetoutStackElement());
	}

	public PrivExprComposition expandPrivExpr(PrivExprElement expr) {

		if (expr == null)
			return null;

		// refine the expression for privilege context.
		if (expr instanceof PrivExprElement) {
			// included the type: Name and ExprSubstitute

			PrivExprComposition plus = new PrivExprComposition();
			PrivExprMergence m = new PrivExprMergence(getBoundNames());
			m.mergePrivilege((PrivExprElement) expr);
			plus.add(m);

			expr = plus;
		} else if (expr instanceof EntityExprElement) {

			PrivExprComposition plus = new PrivExprComposition();
			PrivExprMergence m = new PrivExprMergence(getBoundNames());
			m.mergeEntity((EntityExprElement) expr);
			plus.add(m);

			expr = plus;

		} else if (expr instanceof BoolExprElement) {

			PrivExprComposition plus = new PrivExprComposition();
			PrivExprMergence m = new PrivExprMergence(getBoundNames());
			m.mergeRestriction((BoolExprElement) expr);
			plus.add(m);

			expr = plus;
		}

		this.focus(expr);
		this.setout(expr);

		Object result = getPreviousVisitResult();

		if (result instanceof PrivExprComposition)
			return (PrivExprComposition) result;

		PrivExprComposition plus = new PrivExprComposition();
		if (result instanceof PrivExprMergence) {

			plus.compose((PrivExprMergence) result);
			return plus;
		} else if (result instanceof ExprElement) {

			PrivExprMergence m = new PrivExprMergence(getBoundNames());
			plus.add(m);

			if (result instanceof PrivExprElement)
				m.mergePrivilege((PrivExprElement) result);
			else if (result instanceof BoolExprElement)
				m.mergeRestriction((BoolExprElement) result);
			else if (result instanceof EntityExprElement)
				m.mergeEntity((EntityExprElement) result);

			return plus;
		}

		throw new IllegalStateException("bad normalized: " + result);

	}

	public Name getBearerName() {

		return this.bearer;
	}

	public void boundname(long name) {
		
		boundnames.set(name);
	}

	public void apply(long name, Collection<PrivExprMergence> normterms) {

		applications.put(name, normterms);
	}

	public Collection<PrivExprMergence> getApplication(long name) {

		return applications.get(name);
	}

	public Object setout(Object place) {

		Object visitedResult = null;
		ExprElement bearer = setoutStack.peek().focus;
		setoutStack.push(new SetoutStackElement());
		try {
			if (place == null)
				return null;

			PlaceType<ExprExpandTraveller> ptype;
			ptype = travelmap.find(place.getClass());

			if (ptype == null)
				return place;

			if (place instanceof PrivExprMergence) {
				nearestMergence = (PrivExprMergence) place;
			}

			SetoutStackElement current = setoutStack.peek();

			if (included != null && !included.contains(place.getClass())) {

				logger.finest("skip a place: " + place.getClass());
				current.result = place;
				visitedResult = place;
				return current.result;
			}

			current.neighbor = bearer;
			current.focus = (ExprElement) place;

			int fp = 0;
			if (logger.isLoggable(FINEST)) {
				fp = footprint++;
				logger.finest(format("visiting(%d), %s: %s", fp, ptype
						.getClass().getName(), ftr.toString(place)));
			}

			ptype.visit(this, place);

			visitedResult = current.result;

			if (logger.isLoggable(FINEST)) {
				logger.finest(format("visited result(%d), %s: %s", fp, ptype
						.getClass().getName(), ftr.toString(visitedResult)));
			}

			if (visitedResult == null)
				throw new IllegalStateException(
						"The return value is required: " + ptype);

			if (current.revist) {

				current.revist = false;
				if (logger.isLoggable(FINEST)) {
					logger.finest(format("revisit result(%d), %s: %s", fp,
							ptype.getClass().getName(), ftr
									.toString(visitedResult)));
				}
				this.setout(visitedResult);
			}

			return this.setoutStack.peek().result;
		} finally {
			setoutStack.pop();
			setoutStack.peek().previousResult = visitedResult;
		}
	}

	public PrivExprComposition expandPrivilege(Object expr) {

		if (expr instanceof PrivExprComposition)
			setout(expr);
		else if (expr instanceof PrivExprElement) {
			// included the type: Name and ExprSubstitute

			PrivExprComposition plus = new PrivExprComposition();
			PrivExprMergence m = new PrivExprMergence(getBoundNames());
			m.mergePrivilege((PrivExprElement) expr);
			plus.add(m);
		} else if (expr instanceof EntityExprElement) {

			PrivExprComposition plus = new PrivExprComposition();
			PrivExprMergence m = new PrivExprMergence(getBoundNames());
			m.mergeEntity((EntityExprElement) expr);
			plus.add(m);

		} else if (expr instanceof BoolExprElement) {

			PrivExprComposition plus = new PrivExprComposition();
			PrivExprMergence m = new PrivExprMergence(getBoundNames());
			m.mergeRestriction((BoolExprElement) expr);
			plus.add(m);
		}

		return (PrivExprComposition) this.getPreviousVisitResult();
	}

	public Object getPreviousVisitResult() {

		return this.setoutStack.peek().previousResult;
	}

	public void result(Object result) {

		this.setoutStack.peek().result = result;
	}

	public void focus(ExprElement expr) {

		this.setoutStack.peek().focus = expr;
	}

	public ExprElement getNeighbor() {

		return this.setoutStack.peek().neighbor;
	}

	public void sayVisitAgain() {

		this.setoutStack.peek().revist = true;
	}

	private OpenBitSet boundnames = new OpenBitSet();

	public OpenBitSet getBoundNames() {

		return boundnames;
	}
	
	public ExprExpandTraveller spawn(long boundname) {

		ExprExpandTravellerImpl tr;
		OpenBitSet bn = new OpenBitSet();
		bn.set(boundname);
		tr = new ExprExpandTravellerImpl(this, bn);
		injector.injectMembers(tr);

		return tr;
	}

	public ExprExpandTraveller spawn(OpenBitSet boundnames) {

		ExprExpandTravellerImpl tr;
		tr = new ExprExpandTravellerImpl(this, boundnames);
		injector.injectMembers(tr);

		return tr;
	}

	public ExprExpandTraveller spawn(Class<?>[] clzs) {

		ExprExpandTravellerImpl tr;

		tr = new ExprExpandTravellerImpl(this);
		injector.injectMembers(tr);

		tr.included = new HashSet<Class<?>>();
		for (Class<?> c : clzs) {
			tr.included.add(c);
		}

		return tr;
	}

	private class SetoutStackElement {

		public ExprElement focus;

		public ExprElement neighbor;

		public Object result;

		public Object previousResult;

		boolean revist = false;
	}
}